using System;
using System.Collections.Generic;
using System.Reflection;
using System.Text;
using Kaleida.ServiceMonitor.Framework;
using System.Linq;

namespace Kaleida.ServiceMonitor.Model.Runtime
{
    public class ReflectionOperationFactory : IPreparedRequestFactory, IResponseHandlerFactory
    {
        private readonly List<Assembly> searchAssemblies;

        public ReflectionOperationFactory(IEnumerable<Assembly> searchAssemblies)
        {
            this.searchAssemblies = searchAssemblies.ToList();
        }

        PreparedRequest IPreparedRequestFactory.Build(string name, IList<string> arguments)
        {
            var type = GetOperationType(name, arguments, typeof (PreparedRequest));
            return (PreparedRequest) CreateInstance(type, arguments);
        }

        ResponseHandler IResponseHandlerFactory.Build(string name, IList<string> arguments)
        {
            var type = GetOperationType(name, arguments, typeof(ResponseHandler));
            return (ResponseHandler)CreateInstance(type, arguments);
        }

        public RuntimeEnvironment GetRuntimeEnvironment()
        {
            return new RuntimeEnvironment(GetOperationTypes(typeof (PreparedRequest)), GetOperationTypes(typeof (ResponseHandler)));
        }

        private IList<Type> GetOperationTypes(Type baseType)
        {
            var searchTypes = searchAssemblies.SelectMany(i => i.GetTypes()).ToList();

            var withCorrectBase = new HashSet<Type>(searchTypes.Where(i => i.DerivesFrom(baseType)));
            var withPublicVisibility = new HashSet<Type>(searchTypes.Where(i => i.IsVisible));
            var withMatchingConstructor = new HashSet<Type>(searchTypes.Where(i => i.HasPublicFullyStringParameterisedConstructor()));

            return withCorrectBase.Intersect(withPublicVisibility).Intersect(withMatchingConstructor).ToList();
        }

        private Type GetOperationType(string name, IEnumerable<string> arguments, Type baseType)
        {
            if(!searchAssemblies.Any())
                throw new InvalidOperationException(string.Format("Cannot find a {0} implementation for '{1}'. There are no operation definition assemblies available", baseType.Name, name));

            var searchTypes = searchAssemblies.SelectMany(i => i.GetTypes()).ToList();

            var withMatchingName = new HashSet<Type>(searchTypes.Where(i => i.Name == name.ToPascalCase()));
            var withCorrectBase = new HashSet<Type>(searchTypes.Where(i => i.DerivesFrom(baseType)));
            var withPublicVisibility = new HashSet<Type>(searchTypes.Where(i => i.IsVisible));
            var withMatchingConstructor = new HashSet<Type>(searchTypes.Where(i => i.HasPublicFullyStringParameterisedConstructor(arguments.Count())));

            var candidateTypes = withMatchingName.Intersect(withCorrectBase).Intersect(withPublicVisibility).Intersect(withMatchingConstructor);

            var type = candidateTypes.FirstOrDefault();

            if (type == null)
            {
                var message = new StringBuilder();
                message.AppendFormat("Cannot find a {0} implementation for '{1}' within these search assemblies:\r\n", baseType.Name, name);
                message.Append(searchAssemblies.Select(i => i.FullName).ToBulletedList());
                message.Append("\r\n\r\n");
                message.AppendFormat("The .NET implementation must be named '{0}', ", name.ToPascalCase());
                message.AppendFormat("derive from {0}, ", baseType.Name);
                message.AppendFormat("have public visibility and a public constructor containing all string parameters.\r\n\r\n");

                if (withMatchingName.Any())
                {
                    message.AppendFormat("The following type(s) have the right name but are not quite right:\r\n");
                    message.Append(withMatchingName.Select(i => GetCriteriaAnalysis(i, name, arguments.ToList(), baseType)).Join("\r\n"));
                }

                throw new InvalidOperationException(message.ToString());
            }
            return type;
        }

        internal static string GetCriteriaAnalysis(Type type, string name, IList<string> arguments, Type baseType)
        {
            var message = new StringBuilder();

            message.AppendFormat("  {0}:\r\n", type.FullName);
            message.AppendFormat("    Has correct name? {0}\r\n", (type.Name == name.ToPascalCase()).ToBallotBox());
            message.AppendFormat("    Derives from {0}? {1}\r\n", baseType, type.DerivesFrom(baseType).ToBallotBox());
            message.AppendFormat("    Publicly visibile? {0}\r\n", type.IsVisible.ToBallotBox());
            message.AppendFormat("    Public constructor with {0} string parameter(s)? {1}\r\n", arguments.Count(), type.HasPublicFullyStringParameterisedConstructor(arguments.Count()).ToBallotBox());

            return message.ToString();
        }

        private static object CreateInstance(Type type, IEnumerable<string> arguments)
        {
            try
            {

                return Activator.CreateInstance(type, arguments.Select(i => (object)i).ToArray());
            }
            catch (TargetInvocationException exception)
            {
                throw exception.InnerException;
            }
        }
    }
}